Skip to content

Add folder quota#23

Open
Vicente-Cheng wants to merge 7 commits intopolrstorage:mainfrom
Vicente-Cheng:add-folder-quota
Open

Add folder quota#23
Vicente-Cheng wants to merge 7 commits intopolrstorage:mainfrom
Vicente-Cheng:add-folder-quota

Conversation

@Vicente-Cheng
Copy link
Copy Markdown
Collaborator

related: #22

@gemini-code-assist
Copy link
Copy Markdown

Important

Installation incomplete: to start using Gemini Code Assist, please ask the organization owner(s) to visit the Gemini Code Assist Admin Console and sign the Terms of Services.

@Vicente-Cheng Vicente-Cheng marked this pull request as ready for review April 24, 2026 19:02
@polrstorage polrstorage deleted a comment from gemini-code-assist Bot Apr 24, 2026
@Vicente-Cheng Vicente-Cheng requested review from Copilot and kchiu April 24, 2026 19:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements first-level subdirectory (“PVC”) folder quotas for the local FSAL to support Kubernetes PVC size enforcement, including persistence, FSSTAT reporting, enforcement on write/remove/truncate/rename, and CI smoke coverage.

Changes:

  • Add quota configuration (including size parsing + bootstrap entries) and wire it through backend creation and startup.
  • Introduce QuotaManager (redb persistence + in-memory cache) and integrate quota-aware enforcement + quota-aware statvfs() into LocalFilesystem.
  • Add quota smoke test script and run it in the GitHub workflow; update NFS error mapping to return NFS3ERR_DQUOT on quota errors.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Cargo.toml Adds redb dependency for quota persistence.
src/config.rs Adds QuotaConfig, bootstrap config, and parse_size() helper with tests.
src/lib.rs Exposes config module from the library crate.
src/fsal/mod.rs Adds quota module, FsStats, statvfs() trait method, and quota bootstrap/reconcile hooks.
src/fsal/quota.rs New quota manager with redb-backed storage, cache, and reconciliation helpers + unit tests.
src/fsal/local/mod.rs Wires quota manager into local FSAL; enforces quota on IO ops; implements quota-aware statvfs(), bootstrap, and reconciliation + tests.
src/nfs/fsstat.rs Switches FSSTAT to use backend statvfs() instead of hardcoded values.
src/nfs/write.rs Maps “Quota exceeded” failures to NFS3ERR_DQUOT.
src/nfs/setattr.rs Maps “Quota exceeded” failures to NFS3ERR_DQUOT.
src/nfs/rename.rs Maps “Quota exceeded” failures to NFS3ERR_DQUOT; updates tests for FSAL ctor change.
src/nfs/remove.rs Updates tests for FSAL ctor change.
src/nfs/mkdir.rs Updates tests for FSAL ctor change.
src/nfs/rmdir.rs Updates tests for FSAL ctor change.
src/nfs/readdirplus.rs Updates tests for FSAL ctor change.
src/main.rs Logs quota config; wires quota config into backend; applies bootstrap; schedules reconciliation.
arcticwolf.example.toml Documents quota configuration and bootstrap example.
tests/test_nfs_quota.sh Adds end-to-end quota smoke test using kernel NFS client + df.
.github/workflows/nfstest-factory.yml Enables quota in k8s deployment during CI and runs the new smoke test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs Outdated
Comment thread tests/test_nfs_quota.sh Outdated
Comment thread src/fsal/local/mod.rs
Comment thread src/fsal/quota.rs Outdated
Comment thread src/fsal/local/mod.rs Outdated
Comment thread src/fsal/quota.rs Outdated
Comment thread src/fsal/quota.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs Outdated
Comment thread src/fsal/local/mod.rs
Comment thread src/fsal/quota.rs Outdated
Comment thread src/fsal/quota.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs Outdated
Comment thread tests/test_nfs_quota.sh Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/quota.rs
Comment on lines +286 to +296
pub async fn apply_bootstrap(&self, bootstrap: &HashMap<String, String>) -> Result<()> {
for (dir, size_str) in bootstrap {
if self.get_quota_info(dir).await.is_some() {
tracing::debug!("Quota bootstrap '{}': already present, skipped", dir);
continue;
}
let limit = crate::config::parse_size(size_str)
.with_context(|| format!("Invalid bootstrap size for '{}'", dir))?;
self.set_quota(dir, limit).await?;
tracing::info!("Quota bootstrap: installed '{}' = {} bytes", dir, limit);
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply_bootstrap() accepts arbitrary strings as quota directory keys, but the quota design/docs assume a first-level subdirectory name. Keys containing path separators (e.g. "a/b"), empty components, or ".." won’t match resolve_quota_dir() semantics and can make reconciliation scan unexpected paths under the export. Consider validating bootstrap keys (and/or set_quota) to ensure they are a single path component (no '/' or '\', not '.'/'..') and return a clear error when invalid.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs
Comment thread src/fsal/quota.rs
Comment thread src/main.rs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs
Comment thread src/fsal/local/mod.rs
Comment thread .github/workflows/nfstest-factory.yml Outdated
Comment thread arcticwolf.example.toml Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/nfs/write.rs:74

  • e.to_string() is called repeatedly in the error-mapping chain, allocating a new String each time. Store let msg = e.to_string(); once (as rename.rs already does) and run all contains(...) checks against that to reduce overhead and keep the mapping logic consistent.
            let error_status = if e.to_string().contains("not found")
                || e.to_string().contains("Invalid handle")
            {
                nfsstat3::NFS3ERR_STALE
            } else if e.to_string().contains("Not a file") {
                nfsstat3::NFS3ERR_ISDIR
            } else if e.to_string().contains("Permission denied") {
                nfsstat3::NFS3ERR_ACCES
            } else if e.to_string().contains("Quota exceeded")
                || e.to_string().contains("Disk quota exceeded")
            {
                // Two cases: our own QuotaManager prefixes errors with
                // "Quota exceeded ...", and Linux's EDQUOT (which the
                // underlying filesystem may return when an OS-level
                // project/user quota is in effect) renders as
                // "Disk quota exceeded".
                nfsstat3::NFS3ERR_DQUOT
            } else if e.to_string().contains("No space") {
                nfsstat3::NFS3ERR_NOSPC
            } else if e.to_string().contains("Read-only") {
                nfsstat3::NFS3ERR_ROFS

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs
Comment thread src/fsal/local/mod.rs Outdated
Comment thread src/nfs/setattr.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs
Comment thread src/fsal/local/mod.rs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/local/mod.rs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/fsal/quota.rs
  Adds QuotaConfig (enabled, db_path, bootstrap map) and a parse_size
  helper that turns strings like "10GB" / "500MB" into byte counts.
  Pulls in redb for the persistent store landed later.

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
  Introduces QuotaManager: a per-subdirectory byte-quota tracker
  backed by redb and an in-memory cache, keyed by the first-level
  subdirectory name under the export root.

  Cache mutations and the redb commit are held under the same write
  lock with rollback on persistence failure, so the cache is never
  observable in a state that was not also persisted. Stored keys are
  validated at ingress and again on load, so corrupted entries with
  path separators or `..` cannot slip into the cache where a later
  scan could escape the export root.

  check_quota returns "Quota exceeded" errors so NFS handlers can map
  them to NFS3ERR_DQUOT.

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
  Adds FsStats and Filesystem::statvfs, implemented by
  LocalFilesystem via libc::statvfs in spawn_blocking. The NFS FSSTAT
  handler now reports the real underlying filesystem to clients
  instead of fixed placeholders.

  Byte multiplier prefers f_frsize, falls back to f_bsize when zero,
  with saturating_mul to guard against overflow.

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
  Wires QuotaManager into LocalFilesystem. new() takes an
  Option<&QuotaConfig>; when enabled, a manager is opened against the
  configured redb database. BackendConfig gains with_quota().

  The DB path is rejected if it resolves under the export tree —
  otherwise NFS clients could tamper with quota state via normal
  filesystem operations. Containment is checked in absolute terms
  with a normalize_path helper that safely collapses `..`.

  statvfs reports quota-aware byte counts for paths inside a quota
  directory; inode counts and paths outside any quota fall back to
  libc::statvfs.

  Also exposes `pub mod config` in lib.rs and plumbs the quota block
  through main.

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
  Wires QuotaManager into write, remove, setattr_size, and rename.

  Accounting is in allocated bytes (st_blocks * 512) rather than
  logical length, closing the sparse-file bypass. Writes pre-check
  data.len() and adjust by the real delta after the operation;
  truncate-down releases space; cross-quota rename walks the source
  footprint once and transfers usage after the rename.

  Two stat helpers carry the semantics needed:
    * allocated_bytes_following — for write/setattr_size, follows
      symlinks just like the kernel-level open does.
    * refundable_bytes_on_unlink — for remove, refunds only for
      regular files with nlink == 1 so symlinks and hard-linked
      files cannot be used to drift the counter below reality.

  quota_target canonicalises the path so a cross-PVC symlink charges
  the PVC where the data actually lives. write and setattr_size
  revalidate the resolved path before opening, blocking stale-handle
  plus symlink-swap escapes from the export.

  Post-FS quota updates are best-effort — a redb error after the
  mutation has succeeded is logged, not propagated. NFS handlers for
  WRITE/SETATTR/RENAME map both "Quota exceeded" and Linux's "Disk
  quota exceeded" to NFS3ERR_DQUOT.

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
  Adds scan_and_reconcile and reconcile_all on QuotaManager that walk
  each tracked quota directory, recompute its real byte footprint,
  and overwrite the cached + persisted usage. Repairs drift caused by
  out-of-band changes or lost updates.

  The directory walker is shared with cross-quota rename: the
  previous duplicate path_size and compute_dir_size collapse into one
  allocated_path_size in the quota module, using symlink_metadata so
  the walk cannot escape the subtree.

  The scan runs without the quota lock; the cache update and redb
  commit are serialised under the write lock with rollback on
  failure, preventing a concurrent add_usage between the scan and the
  persist from being silently overwritten.

  LocalFilesystem::start_quota_reconciliation is exposed through the
  Filesystem trait (default no-op) so main can fire-and-forget the
  scan on startup. Missing directories reconcile to zero, and
  per-directory failures are logged without aborting the whole pass.

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
  Rounds out folder quota with declarative bootstrap and an
  end-to-end smoke test driven by a real kernel NFS client.

  QuotaConfig gains a [quota.bootstrap] TOML block. apply_bootstrap
  installs each entry only when no quota record exists, so editing
  the block does not clobber tracked usage. Keys are validated via
  the shared validate_quota_dir helper (rejecting empty, ".", "..",
  path separators, NUL bytes). main applies the bootstrap before
  accepting traffic, gated on quota.enabled.

  The QuotaManager module's umbrella `#![allow(dead_code)]` is
  removed now that every method has a runtime caller; remove_quota
  keeps a per-item allow because it is only exercised by tests but
  kept for a future admin path.

  tests/test_nfs_quota.sh exercises under-limit write, over-limit
  EDQUOT, remove-then-write, truncate-then-write, and `df` reporting.
  The CI workflow patches the deployment ConfigMap to enable quota
  and bootstrap a 1MB limit on pvc-quota-test (with the redb file
  kept outside the export so it cannot be tampered with over NFS),
  then runs both the smoke test and nfstest_posix.

  arcticwolf.example.toml documents the new [quota] section.

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants